package mbsl.util;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * A collection of utility methods for helping to identify and deal with scene release names.
 */
public final class ReleaseNameParser {
	/**
	 * Attempts to parse a release name into its components.
	 * <p>
	 * The input doesn't necessarily have to be a release name, as it will go
	 * through the procedure anyway. The procedure is as follows:<br>
	 * 1. Attempt to isolate the group name from the release name.<br>
	 * 2. Split the remaining string into individual words and signs.<br>
	 * 3. Remove any recognised release keywords, like PROPER.<br>
	 * 4. Set proper capitalisation:<br>
	 * 4.1. Convert Roman numerals to upper case.<br>
	 * 4.2. Convert stop words, like <i>for</i>, to lower case.<br>
	 * 4.3. Convert the rest to first char upper case, and rest lower.<br>
	 * 5. Concatenate the words into a string.
	 * 
	 * @param pReleaseName
	 *            The full release name, for example the name of the folder the
	 *            release is in.
	 * @return A String Array, where index 1 is the name, and index 2 is the
	 *         group.
	 */
	public static ReleaseData parse(String pReleaseName) {
		String group = null;
		final StringBuilder name = new StringBuilder(pReleaseName.length());
		List<String> nameParts = new ArrayList<String>();
		// Remove surrounding whitespace.
		pReleaseName = pReleaseName.trim();
		// Find the last dash, signalling the start of the group name.
		int releaseGroupIndex = pReleaseName.lastIndexOf('-');
		// If there is no last dash, or it is the last character, pretend it is
		// beyond the end of the string.
		if (releaseGroupIndex < 0 || releaseGroupIndex == pReleaseName.length() - 1)
			releaseGroupIndex = pReleaseName.length();
		// Extract the word after the last dash - this is the group name.
		if (releaseGroupIndex >= 1 && releaseGroupIndex < pReleaseName.length() - 1)
			group = pReleaseName.substring(releaseGroupIndex + 1);

		// Isolate and extract each of the remaining words.
		for (String part : pReleaseName.substring(0, releaseGroupIndex).split("[\\._ ]+"))
			// We'll interpret dot, underscore, and space as word separators.
			nameParts.add(part);

		// Remove those of them which strictly identifies the nature of the
		// release.
		removeReleaseWords(nameParts);
		// For each of the remaining words:
		for (int i = 0; i < nameParts.size(); i++) {
			String part = nameParts.get(i);
			// If it's a Roman numeral, convert it to upper case.
			if (ROMAN_NUMERAL_PATTERN.matcher(part).matches())
				name.append(part.toUpperCase());
			// If it's a stop word, unless it's also the first word, convert it
			// to lower case.
			else if (i != 0 && !shouldCapitalise(part))
				name.append(part.toLowerCase());
			// Otherwise, capitalise the first letter, and decapitalise the
			// rest.
			else {
				name.append(Character.toUpperCase(part.charAt(0)));
				name.append(part.substring(1).toLowerCase());
			}
			// And unless this is the last word, add a space.
			if (i + 1 != nameParts.size())
				name.append(' ');
		}

		final String groupName = group;

		return new ReleaseData(name.toString(), groupName);
	}
	/**
	 * Tests whether a string is a release name.
	 * <p>
	 * The rules are:<br>
	 * <ul>
	 * <li>1. The string must start with a number or a letter.
	 * <li>2. The substring between the first letter and the last dash must only
	 * contain letters, numbers, or either of the signs underscore, dot, or
	 * space.
	 * <li>3. The string must end with a dash (-) preceeded by a letter or
	 * number, and immediately followed by a series of numbers and letters with
	 * no white spaces.
	 * </ul>
	 * Note that Strings passed to the parser in no way are required to follow
	 * these rules. It will give its best shot no matter what. This method is by
	 * design much more conservative.
	 * 
	 * @param pString
	 *            The string to test.
	 * @return True if this is a release name.
	 */
	public static boolean isRelease(String pString) {
		return RELEASE_PATTERN.matcher(pString).matches();
	}

	/** Release Data */
	public static class ReleaseData {
		private ReleaseData(String pTitle, String pGroupName) {
			mTitle = pTitle;
			mGroupName = pGroupName;
		}
		/**
		 * The name of the release group.
		 * <p>
		 * Returns null if no group name was found.
		 */
		public String getGroupName() {
			return mGroupName;
		}
		/** The title */
		public String getTitle() {
			return mTitle;
		}

		public String toString() {
			return String.format("%s, %s", getTitle(), getGroupName());
		}
		
		private String mGroupName;
		private String mTitle;
	}

	/**
	 * The pattern used to recognise Roman numerals. The pattern is not
	 * constructed by this class' author, but it is public domain.
	 */
	private static Pattern ROMAN_NUMERAL_PATTERN = Pattern.compile("^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", Pattern.CASE_INSENSITIVE);

	/** Removes the RELEASE_WORDS from the end of the word list */
	private static void removeReleaseWords(List<String> pWords) {
		for (int i = pWords.size() - 1; i > 0; i--) {
			boolean doRemove = false;

			for (String word : RELEASE_WORDS)
				if (pWords.get(i).equalsIgnoreCase(word))
					doRemove = true;

			if (doRemove)
				pWords.remove(i);
			else
				return;
		}
	}

	// Some of these words are normal English words, which will exist in some
	// product titles. It is however unlikely that they'll appear at the end of
	// a title ("The Magnificent Read", "My Mother is Proper",
	// "Pain is Internal", etc...), and I feel it's better to include them on
	// the list, as there are a lot of releases marked internal, (real) proper,
	// and final, and there's always the idiot who puts "READ.NFO" in his
	// release as well.
	/** A list of release words */
	private static final String[] RELEASE_WORDS = new String[] {"proper", "final", "real", "internal", "dvd9", "dvd", 
		"repack", "iso", "clone", "clonecd", "clonedvd", "nfo", "read", "readnfo", 
		"multilingual", "multilanguage", "dvdrip", "hdtv", "xvid", "divx"};

	/** Returns true if this word isn't on the NOCAPITALIZE list */
	private static boolean shouldCapitalise(String pWord) {
		for (String word : NOCAPITALISE)
			if (pWord.equalsIgnoreCase(word))
				return false;

		return true;
	}

	/** A list of words that shouldn't be capitalised. */
	private static final String[] NOCAPITALISE = new String[] { "of", "a", "at", "in", "and", "for", "from", "or", "the", "over", "to" };
	/** The pattern used to verify release names */
	private static Pattern RELEASE_PATTERN = Pattern.compile("[a-z0-9]+([a-z0-9_\\.\\-\\ ]*[a-z0-9]+)?\\-[a-z0-9]+", Pattern.CASE_INSENSITIVE);

	private ReleaseNameParser() {
	}
}
